#include "tv_meta.h"
#include <vector>
#include <list>
#include <time.h>
#include <sstream>

#define QUEUE_MAX 0xFFFFFF
#define HISTORY_MAX 2
#define ATOMIC_LOOP_GUARD 1024
#define UTILITY_THREAD_ID ('x' - 'a')

struct callstack_s {
	int calling_thread_id;
	std::vector< int > line_indices;
	void print(std::ostream & output) {
		auto & li = line_indices;
		output << "thread_" << char('a' + calling_thread_id) << '{';
		for (unsigned i = 0; i < li.size(); ++i) {
			output << li[i];
			if (i != li.size() - 1)
				output << ',';
		}
		output << "}";
	}
};

unsigned _global_step = 0;

class _context_element_c {
	std::list< std::tuple< int, callstack_s, std::string > > callstacks;
public:
	void touch(callstack_s & cs, std::string aux) {
		while (callstacks.size() > HISTORY_MAX)
			callstacks.pop_back();
		callstacks.push_front(std::make_tuple(_global_step, cs, aux));
	}
	void print(std::ostream & output) {
		for (auto p = callstacks.begin(); p != callstacks.end(); ++p) {
			output << std::get<0>(*p) << ":";
			std::get<1>(*p).print(output);
			output << " => " << std::get<2>(*p);
			output << "\r\n";
		}
	}
};

class _event_c
	: public _context_element_c {
	bool var_is_set;
public:
	_event_c() : var_is_set(false) {}
	bool is_set() const {
		return var_is_set;
	}
	void set(callstack_s & cs) {
		touch(cs, "set");
		var_is_set = true;
	}
	void reset(callstack_s & cs) {
		touch(cs, "reset");
		var_is_set = false;
	}
};

class _queue_c
	: public _context_element_c {
	int var_items;
public:
	_queue_c() : var_items(0) {}
	void push(tv_token_s const & tok, callstack_s & cs) {
			 if (tok.type == TT_SOME)
			var_items += rand() % 128;
		else if (tok.type == TT_ONE)
			var_items += 1;
		else _ASSERT(false);
		if (var_items > QUEUE_MAX)
			var_items = QUEUE_MAX;

		std::stringstream desc;
		desc << "count " << var_items;
		touch(cs, desc.str() );
	}
	void pop(tv_token_s const & tok, callstack_s & cs) {
		if (tok.type == TT_SOME) {
			int c = rand() % 2;
			if (c == 0)
				var_items = 0;
			else var_items -= rand() % 128;
		}  else if (tok.type == TT_ONE)
			var_items -= 1;
		else _ASSERT(false);
		if (var_items < 0)
			var_items = 0;

		std::stringstream desc;
		desc << "count " << var_items;
		touch(cs, desc.str());
	}
	bool empty() const {
		return var_items == 0;
	}	
};

class _context_c {
	std::map< int, _event_c > var_events;
	std::map< int, _queue_c > var_queues;
public:
	_queue_c * get_queue(tv_token_s const & token) {
		_ASSERT(token.type == TT_QUEUE_);
		return &var_queues[token.arg_int];
	}
	_event_c * get_event(tv_token_s const & token) {
		_ASSERT(token.type == TT_EVENT_);
		return &var_events[token.arg_int];
	}
	void print(std::ostream & out) {
		for (auto & p : var_events) {
			out << "event_" << char('a' + p.first) << ":";
			out << "\r\n";
			p.second.print(out);
		}
		for (auto & p : var_queues) {
			out << "queue_" << char('a' + p.first) << ":";
			out << "\r\n";
			p.second.print(out);
		}
	}
};

class tv_meta_c::context_c::thread_c {
	struct call_s {
		std::shared_ptr< tv_instructions_s > instructions;
		int position;
	};
	std::vector< call_s > var_callstack;
	int var_curr_line;
	tv_meta_c * var_tvm;
	int var_id;
	_context_c * var_context;
public:
	thread_c(int id, tv_meta_c * meta, _context_c * context)
		: var_id(id)
		, var_tvm(meta)
		, var_curr_line(0)
		, var_context(context) {
	}
	void step(bool start_new) {
		++_global_step;
		if (!start_new && var_callstack.empty())
			return;

		if (!var_callstack.empty()) {
			_execute_next();
		} else {
			_begin();
			_execute_next();
		}
	}
	bool idle() const {
		return var_callstack.empty();
	}
	void run_atomic(std::shared_ptr< tv_instructions_s > const & instructions) {
		_ASSERT(var_callstack.empty());

		call_s call = { instructions, 0 };
		var_callstack.push_back(call);

		for (int i = 0; !idle(); ++i) {
			_execute_next();
			if (i > ATOMIC_LOOP_GUARD) {
				var_tvm->var_result.errors.push_back("loop guard overflow");
				break;
			}
		}
	}

private:
	void _begin() {
		var_curr_line = 0;
		auto & opts = var_tvm->var_entry_points[var_id];
		int picked = rand() % opts.size();
		var_callstack.clear();
		
		call_s call = { opts[picked], 0 };
		var_callstack.push_back(call);
	}
	void _execute_next() {
		int index = var_callstack.size() - 1;
		auto & item = var_callstack[index];
		if (item.position == item.instructions->lines.size()) {
			var_callstack.pop_back();
		} else {
			int proc_pos = item.position++;

			// note this invalidates item
			_process(item.instructions->lines[proc_pos]);
		}
	}
	void _process(tv_line_s const & line) {
		switch (line.tokens[0].type) {
		case TT_RESET: 
			var_context->get_event(line.tokens[1])->reset(_callstack());
			break;
		case TT_SET:
			var_context->get_event(line.tokens[1])->set(_callstack());
			break;
		case TT_POP:
			var_context->get_queue(line.tokens[2])->pop(line.tokens[1], _callstack());
			break;
		case TT_PUSH:
			var_context->get_queue(line.tokens[2])->push(line.tokens[1], _callstack());
			break;
		case TT_VERIFY:
			{
				thread_c tmp(UTILITY_THREAD_ID, var_tvm, var_context);
				tmp.run_atomic(var_tvm->var_conditions[line.tokens[1].arg_int]);
			}
			break;
		case TT_CALL:
			{
				auto called = var_tvm->var_named_functions[line.tokens[1].to_string()];
				call_s added = { called, 0 };
				var_callstack.push_back(added);
			}
			break;
		case TT_REQUIRE:
			{
				_context_element_c * elm = nullptr;
				if (!_eval_cond(line, 1, &elm))
				{
					auto cs = _callstack();

					std::stringstream error_message;
					error_message << "validation failed on line ";
					cs.print(error_message);
					error_message << " touch history\r\n";
					var_context->print(error_message);
					var_tvm->var_result.errors.push_back(error_message.str());
					throw std::exception("validation failed");
				}
			}
			break;
		case TT_IF:
			{
				if (_eval_cond(line, 1))
				{
					_ASSERT(line.tokens.back().type == TT_FUNC_GEN);
					auto called = var_tvm->var_named_functions[line.tokens.back().arg_str];
					call_s added = { called, 0 };
					var_callstack.push_back(added);
				}
			}
			break;
		}
	}
	bool _eval_cond(tv_line_s const & line, int offset, _context_element_c ** evaluated = nullptr) {
		auto & t = line.tokens;
		if (t[offset].type == TT_EVENT_) {

			auto e = var_context->get_event(t[offset]);
			if (nullptr != evaluated)
				*evaluated = e;

			bool parity = t[offset + 1].type == TT_IS
				? true
				: t[offset + 1].type == TT_IS_NOT
				? false
				: throw std::exception("bad condition");

			if (t[offset + 2].type == TT_SET) {
				return e->is_set() == parity;
			} else throw std::exception("bad condition");
		
		} else if (t[offset].type == TT_QUEUE_) {

			auto q = var_context->get_queue(t[offset]);
			if (nullptr != evaluated)
				*evaluated = q;

			bool parity = t[offset + 1].type == TT_IS
				? true
				: t[offset + 1].type == TT_IS_NOT
				? false
				: throw std::exception("bad condition");

			if (t[offset + 2].type == TT_EMPTY) {
				return q->empty() == parity;
			} else throw std::exception("bad condition");

		}
		throw new std::exception("bad condition");
	}
	callstack_s _callstack() const {
		callstack_s result;
		result.calling_thread_id = var_id;
		for (unsigned i = 0; i < var_callstack.size(); ++i) {
			auto & inf = var_callstack[i];

			int idx = inf.position - 1;
			if (idx < 0)
				idx = 0;
			result.line_indices.push_back(inf.instructions->lines[idx].line_index);
		}
		return result;
	}
};

tv_meta_c::context_c::context_c(tv_meta_c * tvm) 
	: var_meta(tvm) {
}
tv_meta_c::context_c::~context_c() {

}
tv_result_s tv_meta_c::context_c::run(int time_in_seconds) {

	var_meta->var_result.errors.clear();
	_context_c context;

	// lets figure out our threads
	std::vector< thread_c * > threads;
	for (auto i = var_meta->var_entry_points.begin(); i != var_meta->var_entry_points.end(); ++i) {
		threads.push_back(new thread_c(i->first, var_meta, &context));
	}

	time_t start;
	time(&start);

	enum run_mode_e {
		RUN_SEQ,
		RUN_RANDOM,
	} mode = RUN_RANDOM;
	
	_global_step = 0;
	thread_c util_thread(UTILITY_THREAD_ID, var_meta, &context);
	int check_idle_period = 16;

	try {
		while (true) {

			bool run_to_idle = (rand() % check_idle_period) == 0;

			while (true) {
				for (unsigned i = 0; i < threads.size(); ++i) {
					int instructions_to_run = 1;
					if (mode == RUN_RANDOM) {
						instructions_to_run = rand() % 4;
						while (rand() % 3 == 0) {
							instructions_to_run += rand() % 4;
						}
					}
					for (int j = 0; j < instructions_to_run; ++j) {
						threads[i]->step(!run_to_idle);

						for (int i : var_meta->var_always) {
							util_thread.run_atomic(var_meta->var_conditions[i]);
						}
					}
				}
				if (run_to_idle) {
					bool all_idle = true;
					for (unsigned i = 0; i < threads.size(); ++i) {
						if (!threads[i]->idle()) {
							all_idle = false;
							break;
						}
					}
					if (all_idle) {
						for (int i : var_meta->var_idle) {
							util_thread.run_atomic(var_meta->var_conditions[i]);
						}
						break;
					}

				} else break;

			}

			time_t now;
			time(&now);
			auto diff = difftime(now, start);
			if (diff > time_in_seconds)
				break;
		}
	} catch (std::exception ex) {
		// empty
	}

	for (auto t : threads)
		delete t;

	return var_meta->var_result;
}